package vulnerability

import (
	"sort"
	"strings"
	"time"

	"github.com/aquasecurity/trivy-db/pkg/db"
	"github.com/aquasecurity/trivy-db/pkg/log"
	"github.com/aquasecurity/trivy-db/pkg/types"
)

const (
	rejectVulnerability = "** REJECT **"
)

type Vulnerability struct {
	dbc db.Operation
}

func New(dbc db.Operation) Vulnerability {
	return Vulnerability{dbc: dbc}
}

func (v Vulnerability) GetDetails(vulnID string) map[types.SourceID]types.VulnerabilityDetail {
	details, err := v.dbc.GetVulnerabilityDetail(vulnID)
	if err != nil {
		log.Warn("Failed to get vulnerability detail", log.Err(err))
		return nil
	} else if len(details) == 0 {
		return nil
	}
	return details
}

func (Vulnerability) IsRejected(details map[types.SourceID]types.VulnerabilityDetail) bool {
	return getRejectedStatus(details)
}

func (Vulnerability) Normalize(vulnID string, details map[types.SourceID]types.VulnerabilityDetail) types.Vulnerability {
	return types.Vulnerability{
		Title:            getTitle(details),
		Description:      getDescription(details),
		Severity:         getSeverity(details).String(), // TODO: We have to keep this key until we deprecate
		CweIDs:           getCweIDs(details),
		VendorSeverity:   getVendorSeverity(details),
		CVSS:             getCVSS(details),
		References:       getReferences(details),
		PublishedDate:    getPublishedDate(vulnID, details),
		LastModifiedDate: getLastModifiedDate(vulnID, details),
	}
}

func getCVSS(details map[types.SourceID]types.VulnerabilityDetail) types.VendorCVSS {
	vc := make(types.VendorCVSS)
	for vendor, detail := range details {
		if (detail.CvssVector == "" || detail.CvssScore == 0) && (detail.CvssVectorV3 == "" || detail.CvssScoreV3 == 0) && (detail.CvssVectorV40 == "" || detail.CvssScoreV40 == 0) {
			continue
		}
		vc[vendor] = types.CVSS{
			V2Vector:  detail.CvssVector,
			V3Vector:  detail.CvssVectorV3,
			V40Vector: detail.CvssVectorV40,
			V2Score:   detail.CvssScore,
			V3Score:   detail.CvssScoreV3,
			V40Score:  detail.CvssScoreV40,
		}
	}
	return vc
}

func getVendorSeverity(details map[types.SourceID]types.VulnerabilityDetail) types.VendorSeverity {
	vs := make(types.VendorSeverity)
	for vendor, detail := range details {
		switch {
		case detail.SeverityV40 != types.SeverityUnknown:
			vs[vendor] = detail.SeverityV40
		case detail.SeverityV3 != types.SeverityUnknown:
			vs[vendor] = detail.SeverityV3
		case detail.Severity != types.SeverityUnknown:
			vs[vendor] = detail.Severity
		case detail.CvssScoreV40 > 0:
			vs[vendor] = scoreToSeverity(detail.CvssScoreV40)
		case detail.CvssScoreV3 > 0:
			vs[vendor] = scoreToSeverity(detail.CvssScoreV3)
		case detail.CvssScore > 0:
			vs[vendor] = scoreToSeverity(detail.CvssScore)
		}
	}
	return vs
}

func getSeverity(details map[types.SourceID]types.VulnerabilityDetail) types.Severity {
	for _, source := range AllSourceIDs {
		switch d, ok := details[source]; {
		case !ok:
			continue
		case d.CvssScoreV40 > 0:
			return scoreToSeverity(d.CvssScoreV40)
		case d.CvssScoreV3 > 0:
			return scoreToSeverity(d.CvssScoreV3)
		case d.CvssScore > 0:
			return scoreToSeverity(d.CvssScore)
		case d.SeverityV40 != 0:
			return d.SeverityV40
		case d.SeverityV3 != 0:
			return d.SeverityV3
		case d.Severity != 0:
			return d.Severity
		}
	}
	return types.SeverityUnknown
}

func getTitle(details map[types.SourceID]types.VulnerabilityDetail) string {
	for _, source := range AllSourceIDs {
		d, ok := details[source]
		if !ok {
			continue
		}
		if d.Title != "" {
			return d.Title
		}
	}
	return ""
}

func getDescription(details map[types.SourceID]types.VulnerabilityDetail) string {
	for _, source := range AllSourceIDs {
		d, ok := details[source]
		if !ok {
			continue
		}
		if d.Description != "" {
			return d.Description
		}
	}
	return ""
}

func getCweIDs(details map[types.SourceID]types.VulnerabilityDetail) []string {
	for _, source := range AllSourceIDs {
		d, ok := details[source]
		if !ok {
			continue
		}
		if len(d.CweIDs) != 0 {
			return d.CweIDs
		}
	}
	return nil
}

func getReferences(details map[types.SourceID]types.VulnerabilityDetail) []string {
	references := map[string]struct{}{}
	for _, source := range AllSourceIDs {
		// Amazon contains unrelated references
		if source == Amazon {
			continue
		}
		d, ok := details[source]
		if !ok {
			continue
		}
		for _, ref := range d.References {
			// e.g. "\nhttps://curl.haxx.se/docs/CVE-2019-5481.html\n    "
			ref = strings.TrimSpace(ref)
			for _, r := range strings.Split(ref, "\n") {
				references[r] = struct{}{}
			}
		}
	}
	var refs []string
	for ref := range references {
		refs = append(refs, ref)
	}
	sort.Slice(refs, func(i, j int) bool {
		return refs[i] < refs[j]
	})
	return refs
}

func getPublishedDate(vulnID string, details map[types.SourceID]types.VulnerabilityDetail) *time.Time {
	// We should take PublishedData from
	//  - NVD - for CVE-IDs
	//  - GHSA - for GHSA-IDs
	// cf. https://github.com/aquasecurity/trivy-db/issues/522
	switch {
	case strings.HasPrefix(vulnID, "CVE-"):
		return details[NVD].PublishedDate
	case strings.HasPrefix(vulnID, "GHSA-"):
		return details[GHSA].PublishedDate
	}

	return nil
}

func getLastModifiedDate(vulnID string, details map[types.SourceID]types.VulnerabilityDetail) *time.Time {
	// We should take LastModifiedDate from
	//  - NVD - for CVE-IDs
	//  - GHSA - for GHSA-IDs
	// cf. https://github.com/aquasecurity/trivy-db/issues/522
	switch {
	case strings.HasPrefix(vulnID, "CVE-"):
		return details[NVD].LastModifiedDate
	case strings.HasPrefix(vulnID, "GHSA-"):
		return details[GHSA].LastModifiedDate
	}

	return nil
}

func getRejectedStatus(details map[types.SourceID]types.VulnerabilityDetail) bool {
	for _, source := range AllSourceIDs {
		d, ok := details[source]
		if !ok {
			continue
		}
		if strings.Contains(d.Description, rejectVulnerability) {
			return true
		}
	}
	return false
}

func scoreToSeverity(score float64) types.Severity {
	switch {
	case score >= 9.0:
		return types.SeverityCritical
	case score >= 7.0:
		return types.SeverityHigh
	case score >= 4.0:
		return types.SeverityMedium
	case score > 0.0:
		return types.SeverityLow
	default:
		return types.SeverityUnknown
	}
}

func NormalizePkgName(ecosystem types.Ecosystem, pkgName string) string {
	switch ecosystem {
	case Pip:
		// from https://www.python.org/dev/peps/pep-0426/#name
		// All comparisons of distribution names MUST be case insensitive,
		// and MUST consider hyphens and underscores to be equivalent.
		pkgName = strings.ToLower(pkgName)
		pkgName = strings.ReplaceAll(pkgName, "_", "-")
	case Swift:
		// Swift uses `https://github.com/<author>/<package>.git format
		// But part Swift advisories doesn't `https://` prefix or `.git` suffix
		// e.g. https://github.com/github/advisory-database/blob/76f65b0d0fdac39c8b0e834ab03562b5f80d5b27/advisories/github-reviewed/2023/06/GHSA-qvxg-wjxc-r4gg/GHSA-qvxg-wjxc-r4gg.json#L21
		// https://github.com/github/advisory-database/blob/76f65b0d0fdac39c8b0e834ab03562b5f80d5b27/advisories/github-reviewed/2023/07/GHSA-jq43-q8mx-r7mq/GHSA-jq43-q8mx-r7mq.json#L21
		// Remove them to fit the same format
		pkgName = strings.TrimPrefix(pkgName, "https://")
		pkgName = strings.TrimSuffix(pkgName, ".git")
	case NuGet, Go, Cocoapods:
		// Nuget and Cocoapods are case-sensitive, Go has case-sensitive packages
		// Keep the original case
	default:
		pkgName = strings.ToLower(pkgName)
	}
	return pkgName
}
